package com.mojavelinux.open18;

import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.OrderBy;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.validator.Length;
import org.hibernate.validator.NotNull;
import org.hibernate.validator.Pattern;
import org.hibernate.validator.Size;

/**
 * <p>
 * TODO: a course can either have 9 or 18 holes. If it has all par 3s, then it
 * is a Par 3 course. An accessor method could determine if isPar3().
 * PrePersist/PreUpdate, verify length of holes is 9 or 18.
 * </p>
 * <p>
 * <strong>Recommendation:</strong>
 * <ul>
 * <li>id should have a public getter, and no setter</li>
 * <li>version should have a protected getter, no setter</li>
 * <li>variables of type Collection, List, Set, ... have a public getter and no
 * setter. The public getter preferably return an unmodifiable list.
 * <li>
 * </ul>
 * </p>
 */
@Entity
@Table( name = "course" )
public class Course implements java.io.Serializable {

	private static final long serialVersionUID = 1L;

	private long id;
	
	private int version = 0;

	private String name;

	private CourseAccessType type;

	private GrassType greens;
	
	private GrassType fairways;
	
	private Address address;

	private String uri;

	private String phoneNumber;

	private String description;

	private int yearBuilt;

	private String designer;
	
	private Set<StarRating> starRatings;

	private Set<Hole> holes;
	
	private Set<TeeSet> teeSets;

	public Course() {
		greens = GrassType.UNKNOWN; // perhaps just use null?
		fairways = GrassType.UNKNOWN; // perhaps just use null?
		starRatings = new HashSet<StarRating>();
		holes = new TreeSet<Hole>();
		teeSets = new TreeSet<TeeSet>();
	}

	@Id
	@GeneratedValue( strategy = GenerationType.AUTO )
	@Column( name = "id", updatable = false )
	public long getId() {
		return this.id;
	}

	public void setId( long id ) {
		this.id = id;
	}

	@Version
	@Column( name = "version" )
	public int getVersion() {
		return version;
	}

	protected void setVersion( int version ) {
		this.version = version;
	}

	@Column( name = "name" )
	@Length( min = 1, max = 50 )
	@NotNull
	public String getName() {
		return this.name;
	}

	public void setName( String name ) {
		this.name = name;
	}

	@Column( name = "type" )
	@Enumerated(EnumType.STRING)
	@NotNull
	public CourseAccessType getType() {
		return type;
	}

	public void setType( CourseAccessType type ) {
		this.type = type;
	}

	@Column( name = "greens" )
	@Enumerated(EnumType.STRING)
	@NotNull
	public GrassType getGreens() {
		return greens;
	}

	public void setGreens( GrassType puttingGreenGrass ) {
		this.greens = puttingGreenGrass;
	}

	@Column( name = "fairways" )
	@Enumerated(EnumType.STRING)
	@NotNull
	public GrassType getFairways() {
		return fairways;
	}

	public void setFairways( GrassType fairways ) {
		this.fairways = fairways;
	}

	@Embedded
	public Address getAddress() {
		return address;
	}
	
	public void setAddress( Address address ) {
		this.address = address;
	}

	@Column( name = "uri" )
	@Length( max = 255 )
	@Pattern( regex = "^https?://.+$", message = "validator.custom.url" )
	public String getUri() {
		return this.uri;
	}

	public void setUri( String uri ) {
		this.uri = uri;
	}

	@Column( name = "phone" )
	@Length( min = 10, max = 10 )
	@Pattern( regex = "^\\d*$", message = "validator.custom.digits" )
	public String getPhoneNumber() {
		return this.phoneNumber;
	}

	public void setPhoneNumber( String phoneNumber ) {
		this.phoneNumber = phoneNumber;
	}

	@Column( name = "description", length = 65535 )
	@Length( min = 1, max = 65535 )
	public String getDescription() {
		return this.description;
	}

	public void setDescription( String description ) {
		this.description = description;
	}

	@Column( name = "designer" )
	public String getDesigner() {
		return designer;
	}

	public void setDesigner( String designer ) {
		this.designer = designer;
	}

	@Column( name = "year_built" )
	@Length( min = 4, max = 4 )
	@Pattern( regex = "^\\d*$", message = "validator.custom.digits" )
	public int getYearBuilt() {
		return yearBuilt;
	}

	public void setYearBuilt( int yearBuilt ) {
		this.yearBuilt = yearBuilt;
	}

	@OneToMany( cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "course" )
	//@Fetch( FetchMode.SUBSELECT )
	public Set<StarRating> getStarRatings() {
		return starRatings;
	}

	public void setStarRatings( Set<StarRating> starRatings ) {
		this.starRatings = starRatings;
	}
	
	public void addStarRating( StarRating starRating ) {
		starRating.setCourse( this );
		starRatings.add( starRating );
	}
	
	public void removeStarRating( StarRating starRating ) {
		starRatings.remove( starRating );
	}
	
	@Transient
	public double getAverageGrade() {
		if ( starRatings.size() == 0 ) {
			return 0;
		}
		
		int total = 0;
		for ( StarRating starRating : starRatings ) {
			total += starRating.getGrade();
		}
		
		return (double) total / starRatings.size();
	}

	@OneToMany( cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "course" )
	@Size( min = 0, max = 18 )
	@OrderBy("number asc")
	public Set<Hole> getHoles() {
		return holes;
	}

	public void setHoles( Set<Hole> holes ) {
		this.holes = holes;
	}

	public void addHole( Hole hole ) {
		hole.setCourse( this );
		holes.add( hole );
	}
	
	public void removeHole( Hole hole ) {
		holes.remove( hole );
	}
	
	/**
	 * Get the tees for this golf course.  The tees are ordered by
	 * distance, longest first, so that they appear correctly on the card.
	 */
	@OneToMany( cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "course" )
	@OrderBy("position asc")
	public Set<TeeSet> getTeeSets() {
		return teeSets;
	}

	public void setTeeSets( Set<TeeSet> tees ) {
		this.teeSets = tees;
	}
	
	public void addTeeSet( TeeSet teeSet ) {
		teeSet.setCourse( this );
		teeSets.add( teeSet );
		teeSet.setPosition( teeSets.size() );
	}
	
	public void removeTeeSet( TeeSet teeSet ) {
		teeSets.remove( teeSet );
	}
	
	/**
	 * Return all men's and unisex tee sets.  This method is
	 * provided so that the scorecard can be split appropriately.
	 */
	@Transient
	public Set<TeeSet> getMensAndUnisexTeeSets() {
		@SuppressWarnings("unchecked")
		Set<TeeSet> result = new TreeSet<TeeSet>( CollectionUtils.disjunction( teeSets, getLadiesTeeSets() ) );
		return result;
	}
	
	/**
	 * Return all ladies tee sets.  This method is provided so
	 * that the scorecard can be split appropriately.
	 */
	@Transient
	public Set<TeeSet> getLadiesTeeSets() {
		Set<TeeSet> teeSetsSubSet = new TreeSet<TeeSet>();
		for ( TeeSet teeSet : teeSets ) {
			if ( teeSet.getLadiesRating() != null && teeSet.getLadiesRating().isAssigned() &&
				(teeSet.getMensRating() == null || !teeSet.getMensRating().isAssigned() ) ) {
				teeSetsSubSet.add( teeSet );
			}
		}
		
		return teeSetsSubSet;
	}
	
//    @Transient
//    public Map<Integer, Hole> getHolesByNumber() {
//        Map<Integer, Hole> holesByNumber = new HashMap<Integer, Hole>();
//        for ( Hole hole : holes ) {
//            holesByNumber.put( hole.getNumber(), hole );
//        }
//
//        return holesByNumber;
//    }
    
    @Transient
    public Set<Hole> getHolesOut() {
        Set<Hole> holeSubSet = new TreeSet<Hole>();
        for ( Hole hole : holes ) {
            if ( hole.getNumber() <= 9 ) {
                holeSubSet.add( hole );
            }
        }

        return holeSubSet;
    }

    @Transient
    public Set<Hole> getHolesIn() {
        Set<Hole> holeSubSet = new TreeSet<Hole>();
        for ( Hole hole : holes ) {
            if ( hole.getNumber() > 9 ) {
                holeSubSet.add( hole );
            }
        }

        return holeSubSet;
    }
    
    @Transient
    public int getMensParOut() {
    	int par = 0;
    	for ( Hole hole : holes ) {
    		if ( hole.getNumber() <= 9 ) {
    			par += hole.getMensPar();
    		}
    	}
    	return par;
    }
    
    @Transient
    public int getMensParIn() {
    	int par = 0;
    	for ( Hole hole : holes ) {
    		if ( hole.getNumber() > 9 ) {
    			par += hole.getMensPar();
    		}
    	}
    	return par;
    }
    
    @Transient
    public int getMensParTotal() {
    	return getMensParOut() + getMensParIn();
    }
    
    @Transient
    public int getLadiesParOut() {
    	int par = 0;
    	for ( Hole hole : holes ) {
    		if ( hole.getNumber() <= 9 ) {
    			par += hole.getLadiesPar();
    		}
    	}
    	return par;
    }
    
    @Transient
    public int getLadiesParIn() {
    	int par = 0;
    	for ( Hole hole : holes ) {
    		if ( hole.getNumber() > 9 ) {
    			par += hole.getLadiesPar();
    		}
    	}
    	return par;
    }
    
    @Transient
    public int getLadiesParTotal() {
    	return getLadiesParOut() + getLadiesParIn();
    }
	
    @Transient
    public boolean isLadiesParUnique() {
    	for ( Hole hole : holes ) {
    		if ( !ObjectUtils.equals( hole.getMensPar(),  hole.getLadiesPar() ) ) {
    			return true;
    		}
    	}
    	
    	return false;
    }
    
	@Transient
	public boolean isLadiesHandicapUnique() {
		for ( Hole hole : holes ) {
    		if ( !ObjectUtils.equals( hole.getMensHandicap(), hole.getLadiesHandicap() ) ) {
    			return true;
    		}
    	}
    	
    	return false;
	}
	
	@Transient
	public boolean isScorecardEmpty() {
		if ( this.holes.size() == 0 ) {
			return true;
		}
		
		for ( TeeSet teeSet : teeSets ) {
			if ( teeSet.getTees().size() > 0 ) {
				return false;
			}
		}
		
		return true;
	}
	
    public boolean equals( Object o ) {
        if ( this == o ) {
            return true;
        }

        if ( !( o instanceof Course ) ) {
            return false;
        }

        final Course other = (Course) o;

        // short circuit by comparing ids
        if ( this.id > 0 && other.getId() > 0 ) {
        	return this.id == other.getId();
        }
        
        if ( this.name == null ) {
        	if ( other.getName() != null ) {
        		return false;
        	}
        }
        else if ( !this.name.equals( other.getName() ) ) {
        	return false;
        }
        
        if ( this.address == null ) {
        	if ( other.getAddress() != null ) {
        		return false;
        	}
        }
        else if ( !this.address.equals( other.getAddress() ) ) {
        	return false;
        }
        
        return true;
    }

    public int hashCode() {
    	final int PRIME = 31;
        int result = 1;
        result = PRIME * result + ( name != null ? name.hashCode() : 0 );
        result = PRIME * result + ( address != null ? address.hashCode() : 0 );
    	return result;
    }
	
    public String toString() {
        return new ToStringBuilder( this, ToStringStyle.DEFAULT_STYLE ).
        	append( "id", id ).
            append( "name", name ).
            toString();
    }

}
